package org.movee.spring.data.hugegraph.repository.query;

import org.movee.spring.data.hugegraph.gremlin.GremlinTraversal;
import org.movee.spring.data.hugegraph.query.paramerter.HugeParameterAccessor;
import org.movee.spring.data.hugegraph.repository.query.ExpressionEvaluatingParameterBinder.BindingContext;
import org.movee.spring.data.hugegraph.repository.query.ExpressionEvaluatingParameterBinder.ParameterBinding;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class StringBasedQuery {

    private final String query;

    private final ExpressionEvaluatingParameterBinder parameterBinder;

    private final List<ParameterBinding> queryParameterBindings = new ArrayList<>();


    StringBasedQuery(String query, ExpressionEvaluatingParameterBinder parameterBinder) {

        Assert.hasText(query, "Query must not be empty");
        Assert.notNull(parameterBinder, "ExpressionEvaluatingParameterBinder must not be null");

        this.parameterBinder = parameterBinder;

        this.query = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
                this.queryParameterBindings);

    }

    private ExpressionEvaluatingParameterBinder getParameterBinder() {
        return this.parameterBinder;
    }

    /* (non-Javadoc) */
    protected String getQuery() {
        return this.query;
    }

    GremlinTraversal bindQuery(HugeParameterAccessor parameterAccessor, HugeQueryMethod queryMethod) {

        Assert.notNull(parameterAccessor, "GremlinParameterAccessor must not be null");
        Assert.notNull(queryMethod, "GremlinQueryMethod must not be null");

        List<Object> arguments = getParameterBinder().bind(parameterAccessor,
                new BindingContext(queryMethod, this.queryParameterBindings));

        return ParameterBinder.INSTANCE.bind(getQuery(), arguments);
    }

    enum ParameterBinder {

        INSTANCE;

        private static final String ARG_PLACEHOLDER = "?_param_?";
        private static final Pattern ARG_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARG_PLACEHOLDER));

        public GremlinTraversal bind(String input, List<Object> parameters) {

            StringBuilder result = new StringBuilder();

            int startIndex = 0;
            int currentPosition = 0;
            int parameterIndex = 0;

            Matcher matcher = ARG_PLACEHOLDER_PATTERN.matcher(input);

            while (currentPosition < input.length() && parameterIndex < parameters.size()) {

                if (!matcher.find()) {
                    break;
                }

                int exprStart = matcher.start();

                result.append(input.subSequence(startIndex, exprStart)).append(parameters.get(parameterIndex));

                parameterIndex++;
                currentPosition = matcher.end();
                startIndex = currentPosition;
            }

            String traversal = result.append(input.subSequence(currentPosition, input.length())).toString();

            return GremlinTraversal.createDirectly(traversal);
        }
    }


    enum ParameterBindingParser {

        INSTANCE;

        private static final char CURRLY_BRACE_OPEN = '{';
        private static final char CURRLY_BRACE_CLOSE = '}';

        private static final Pattern INDEX_PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
        private static final Pattern NAMED_PARAMETER_BINDING_PATTERN = Pattern.compile("\\:(\\w+)");
        private static final Pattern INDEX_BASED_EXPRESSION_PATTERN = Pattern.compile("\\?\\#\\{");
        private static final Pattern NAME_BASED_EXPRESSION_PATTERN = Pattern.compile("\\:\\#\\{");

        private static final String ARGUMENT_PLACEHOLDER = "?_param_?";

        public String parseAndCollectParameterBindingsFromQueryIntoBindings(String input,
                                                                            List<ParameterBinding> bindings) {

            if (!StringUtils.hasText(input)) {
                return input;
            }

            Assert.notNull(bindings, "Parameter bindings must not be null");

            return transformQueryAndCollectExpressionParametersIntoBindings(input, bindings);
        }

        private static String transformQueryAndCollectExpressionParametersIntoBindings(String input,
                                                                           List<ParameterBinding> bindings) {

            StringBuilder result = new StringBuilder();

            int startIndex = 0;
            int currentPosition = 0;

            while (currentPosition < input.length()) {

                Matcher matcher = findNextBindingOrExpression(input, currentPosition);

                // no expression parameter found
                if (matcher == null) {
                    break;
                }

                int exprStart = matcher.start();
                currentPosition = exprStart;

                if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN
                        || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) {
                    // eat parameter expression
                    int curlyBraceOpenCount = 1;
                    currentPosition += 3;

                    while (curlyBraceOpenCount > 0 && currentPosition < input.length()) {
                        switch (input.charAt(currentPosition++)) {
                            case CURRLY_BRACE_OPEN:
                                curlyBraceOpenCount++;
                                break;
                            case CURRLY_BRACE_CLOSE:
                                curlyBraceOpenCount--;
                                break;
                            default:
                        }
                    }

                    result.append(input.subSequence(startIndex, exprStart));
                } else {
                    result.append(input.subSequence(startIndex, exprStart));
                }

                result.append(ARGUMENT_PLACEHOLDER);

                if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN
                        || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) {
                    bindings.add(ParameterBinding
                            .expression(input.substring(exprStart + 3, currentPosition - 1), true));
                } else {
                    if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) {
                        bindings.add(ParameterBinding.indexed(Integer.parseInt(matcher.group(1))));
                    } else {
                        bindings.add(ParameterBinding.named(matcher.group(1)));
                    }

                    currentPosition = matcher.end();
                }

                startIndex = currentPosition;
            }

            return result.append(input.subSequence(currentPosition, input.length())).toString();
        }

        @Nullable
        private static Matcher findNextBindingOrExpression(String input, int position) {

            List<Matcher> matchers = new ArrayList<>();

            matchers.add(INDEX_PARAMETER_BINDING_PATTERN.matcher(input));
            matchers.add(NAMED_PARAMETER_BINDING_PATTERN.matcher(input));
            matchers.add(INDEX_BASED_EXPRESSION_PATTERN.matcher(input));
            matchers.add(NAME_BASED_EXPRESSION_PATTERN.matcher(input));

            TreeMap<Integer, Matcher> matcherMap = new TreeMap<>();

            for (Matcher matcher : matchers) {
                if (matcher.find(position)) {
                    matcherMap.put(matcher.start(), matcher);
                }
            }

            return (matcherMap.isEmpty() ? null : matcherMap.values().iterator().next());
        }
    }
}
